Skip to content

React Hooks 核心概念与实战面试题全解析

一、核心要点速览

💡 核心考点

  • Hooks 本质: 让函数组件拥有状态和副作用能力
  • 使用规则: 只能在顶层调用,不能在条件/循环中调用
  • 常用 Hooks: useState、useEffect、useContext、useReducer
  • 性能优化: useCallback、useMemo、useRef
  • 自定义 Hooks: 逻辑复用的最佳实践

二、Hooks 基础概念

1. 为什么需要 Hooks?

javascript
// ========== Class Component 的问题 ==========

// 问题 1: 逻辑复用困难(需要 HOC 或 Render Props)
class UserFetcher extends React.Component {
  state = { user: null, loading: true }
  
  componentDidMount() {
    fetchUser(this.props.userId).then(user => {
      this.setState({ user, loading: false })
    })
  }
  
  render() {
    return this.props.children(this.state)
  }
}

// 使用时嵌套严重
<UserFetcher userId={1}>
  {({ user, loading }) => (
    <PostFetcher userId={1}>
      {({ posts }) => (
        <CommentFetcher postId={posts[0].id}>
          {({ comments }) => (
            <div>{/* 回调地狱 */}</div>
          )}
        </CommentFetcher>
      )}
    </PostFetcher>
  )}
</UserFetcher>


// 问题 2: 复杂组件难以理解
class ComplexComponent extends React.Component {
  componentDidMount() {
    // 订阅事件
    window.addEventListener('resize', this.handleResize)
    // 获取数据
    this.fetchData()
    // 启动定时器
    this.timer = setInterval(this.tick, 1000)
  }
  
  componentDidUpdate(prevProps) {
    // 同样的逻辑又要写一遍
    if (prevProps.userId !== this.props.userId) {
      this.fetchData()
    }
  }
  
  componentWillUnmount() {
    // 清理逻辑分散
    window.removeEventListener('resize', this.handleResize)
    clearInterval(this.timer)
  }
  
  render() { /* ... */ }
}


// ========== Hooks 解决方案 ==========

// 解决 1: 逻辑复用 - 自定义 Hooks
function useUser(userId) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    fetchUser(userId).then(user => {
      setUser(user)
      setLoading(false)
    })
  }, [userId])
  
  return { user, loading }
}

// 使用时清晰简洁
function UserProfile({ userId }) {
  const { user, loading } = useUser(userId)
  const { posts } = usePosts(userId)
  const { comments } = useComments(posts[0]?.id)
  
  if (loading) return <Spinner />
  return <div>{/* 清晰的逻辑 */}</div>
}


// 解决 2: 相关逻辑聚合
function ComplexComponent({ userId }) {
  // 窗口大小逻辑在一起
  useEffect(() => {
    const handleResize = () => console.log(window.innerWidth)
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])
  
  // 数据获取逻辑在一起
  useEffect(() => {
    fetchData(userId)
  }, [userId])
  
  // 定时器逻辑在一起
  useEffect(() => {
    const timer = setInterval(tick, 1000)
    return () => clearInterval(timer)
  }, [])
  
  return <div>{/* ... */}</div>
}

2. Hooks 执行流程图

┌──────────────────────────────────────────────────────────┐
│                 React Hooks 执行流程                     │
└──────────────────────────────────────────────────────────┘

首次渲染:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function Counter() {
  const [count, setCount] = useState(0)     // Hook 1
  
  useEffect(() => {                          // Hook 2
    document.title = `Count: ${count}`
  }, [count])
  
  return <button onClick={() => setCount(count + 1)}>
    {count}
  </button>
}

内部结构:
┌─────────────────────────────────────┐
│  Fiber Node                         │
│  ┌───────────────────────────────┐  │
│  │ memoizedState (Hook 链表)     │  │
│  │                               │  │
│  │ Hook 1:                       │  │
│  │   - memoizedState: 0          │  │
│  │   - queue: { pending... }     │  │
│  │   - next: ────────────────┐   │  │
│  │                           │   │  │
│  │ Hook 2:                   │   │  │
│  │   - memoizedState: null   │   │  │
│  │   - effect: {             │   │  │
│  │       create: fn,         │   │  │
│  │       destroy: undefined, │   │  │
│  │       deps: [count]       │   │  │
│  │     }                     │   │  │
│  │   - next: null ◄──────────┘   │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

更新渲染:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
用户点击按钮 → setCount(1)


┌─────────────────┐
│ 调度重新渲染     │
└────────┬────────┘


┌─────────────────┐
│ 遍历 Hook 链表   │ ← 按顺序读取上次的状态
└────────┬────────┘

         ├─ Hook 1: 读取 count = 0
         │         更新为 1

         └─ Hook 2: 比较 deps [0] vs [1]
                   不同 → 执行 cleanup
                   执行新 effect

关键点:
✓ Hooks 基于链表存储状态
✓ 必须按相同顺序调用
✓ 依赖数组浅比较
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

三、常用 Hooks 详解

1. useState - 状态管理

javascript
// ========== 基础用法 ==========
function Counter() {
  const [count, setCount] = useState(0)
  
  // 直接更新
  const increment = () => setCount(count + 1)
  
  // 函数式更新(推荐,基于最新状态)
  const incrementSafe = () => setCount(prev => prev + 1)
  
  return <button onClick={incrementSafe}>{count}</button>
}


// ========== 惰性初始化 ==========
function ExpensiveComponent() {
  // ✓ 只在初始渲染时计算一次
  const [state, setState] = useState(() => {
    const initialState = someExpensiveComputation(props)
    return initialState
  })
  
  // ✗ 每次渲染都计算(浪费性能)
  const [state, setState] = useState(someExpensiveComputation(props))
}


// ========== 常见陷阱 ==========

// 陷阱 1: 异步更新
function Counter() {
  const [count, setCount] = useState(0)
  
  const handleClick = () => {
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    console.log(count) // 0(还是旧值)
  }
  // 结果: count = 1(不是 3)
  
  // ✓ 正确做法
  const handleClickCorrect = () => {
    setCount(c => c + 1)
    setCount(c => c + 1)
    setCount(c => c + 1)
  }
  // 结果: count = 3
}


// 陷阱 2: 对象状态更新
function Form() {
  const [form, setForm] = useState({ name: '', age: 0 })
  
  const updateName = (name) => {
    // ✗ 错误:丢失其他字段
    setForm({ name })
    
    // ✓ 正确:展开运算符
    setForm(prev => ({ ...prev, name }))
  }
}


// 陷阱 3: 闭包陷阱
function Timer() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    const id = setInterval(() => {
      console.log(count) // 永远是 0(闭包捕获)
    }, 1000)
    return () => clearInterval(id)
  }, []) // 空依赖数组
  
  // ✓ 解决方案 1: 添加依赖
  useEffect(() => {
    const id = setInterval(() => {
      console.log(count)
    }, 1000)
    return () => clearInterval(id)
  }, [count]) // 每次 count 变化都重建定时器
  
  // ✓ 解决方案 2: 函数式更新
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => {
        console.log(c) // 最新值
        return c + 1
      })
    }, 1000)
    return () => clearInterval(id)
  }, [])
}

2. useEffect - 副作用处理

javascript
// ========== 基本语法 ==========
useEffect(() => {
  // 副作用代码
  const subscription = subscribeToData()
  
  // 清理函数(可选)
  return () => {
    subscription.unsubscribe()
  }
}, [dependencies]) // 依赖数组


// ========== 三种模式 ==========

// 模式 1: 每次渲染后执行(无依赖数组)
useEffect(() => {
  console.log('每次渲染都执行')
})


// 模式 2: 仅挂载和卸载时执行(空依赖数组)
useEffect(() => {
  console.log('仅挂载时执行')
  
  return () => {
    console.log('仅卸载时执行')
  }
}, [])


// 模式 3: 依赖变化时执行
useEffect(() => {
  console.log('userId 变化时执行')
  fetchUser(userId)
}, [userId])


// ========== 实际应用场景 ==========

// 场景 1: 数据获取
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    let cancelled = false
    
    async function fetchData() {
      try {
        setLoading(true)
        const data = await fetchUser(userId)
        if (!cancelled) {
          setUser(data)
          setError(null)
        }
      } catch (err) {
        if (!cancelled) {
          setError(err)
        }
      } finally {
        if (!cancelled) {
          setLoading(false)
        }
      }
    }
    
    fetchData()
    
    return () => {
      cancelled = true // 防止组件卸载后更新状态
    }
  }, [userId])
  
  if (loading) return <Spinner />
  if (error) return <Error message={error.message} />
  return <div>{user.name}</div>
}


// 场景 2: 订阅/取消订阅
function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(roomId)
    connection.connect()
    
    return () => {
      connection.disconnect()
    }
  }, [roomId])
  
  return <ChatUI />
}


// 场景 3: DOM 操作
function AutoFocusInput() {
  const inputRef = useRef(null)
  
  useEffect(() => {
    inputRef.current?.focus()
  }, [])
  
  return <input ref={inputRef} />
}


// 场景 4: 事件监听
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  })
  
  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      })
    }
    
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])
  
  return size
}


// ========== 常见错误 ==========

// 错误 1: 忘记添加依赖
function SearchResults({ query }) {
  const [results, setResults] = useState([])
  
  useEffect(() => {
    // ✗ query 变化不会重新请求
    fetch(`/api/search?q=${query}`).then(setResults)
  }, []) // 应该包含 query
  
  // ✓ 正确
  useEffect(() => {
    fetch(`/api/search?q=${query}`).then(setResults)
  }, [query])
}


// 错误 2: 依赖过多导致无限循环
function Counter() {
  const [count, setCount] = useState(0)
  const [data, setData] = useState(null)
  
  // ✗ 无限循环:setData 每次都是新引用
  useEffect(() => {
    fetchData().then(setData)
  }, [data])
  
  // ✓ 正确:只依赖必要的值
  useEffect(() => {
    fetchData().then(setData)
  }, []) // 仅在挂载时执行
}


// 错误 3: 在 effect 中修改状态触发循环
function BadExample() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    setCount(count + 1) // 触发重新渲染 → 再次执行 effect → 无限循环
  }, [count])
}

3. useContext - 跨组件通信

javascript
// ========== 基础用法 ==========

// 1. 创建 Context
const ThemeContext = React.createContext('light')
const UserContext = React.createContext(null)


// 2. 提供值
function App() {
  const [theme, setTheme] = useState('dark')
  const [user, setUser] = useState({ name: 'Vue' })
  
  return (
    <ThemeContext.Provider value={theme}>
      <UserContext.Provider value={user}>
        <MainContent />
      </UserContext.Provider>
    </ThemeContext.Provider>
  )
}


// 3. 消费值
function ThemedButton() {
  const theme = useContext(ThemeContext)
  const user = useContext(UserContext)
  
  return (
    <button className={`btn-${theme}`}>
      Hello, {user.name}
    </button>
  )
}


// ========== 性能优化 ==========

// 问题:Provider 值变化会导致所有消费者重新渲染
function App() {
  // ✗ 每次渲染都创建新对象,导致不必要的重渲染
  return (
    <UserContext.Provider value={{ name: 'Vue', age: 3 }}>
      <Child />
    </UserContext.Provider>
  )
}

// ✓ 使用 useMemo 缓存
function App() {
  const userValue = useMemo(() => ({ name: 'Vue', age: 3 }), [])
  
  return (
    <UserContext.Provider value={userValue}>
      <Child />
    </UserContext.Provider>
  )
}


// ========== 自定义 Hook 封装 ==========
function useTheme() {
  const context = useContext(ThemeContext)
  if (context === undefined) {
    throw new Error('useTheme must be used within ThemeProvider')
  }
  return context
}

function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

4. useReducer - 复杂状态管理

javascript
// ========== 基础用法 ==========
const initialState = { count: 0 }

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    case 'reset':
      return initialState
    default:
      throw new Error(`Unknown action: ${action.type}`)
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)
  
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </>
  )
}


// ========== 惰性初始化 ==========
function init(initialCount) {
  return { count: initialCount }
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    case 'reset':
      return init(action.payload)
    default:
      throw new Error()
  }
}

function Counter({ initialCount }) {
  const [state, dispatch] = useReducer(reducer, initialCount, init)
  
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'reset', payload: initialCount })}>
        Reset
      </button>
    </>
  )
}


// ========== 与 useContext 结合(替代 Redux)==========

// actions.js
export const todoActions = {
  addTodo: (text) => ({ type: 'ADD_TODO', payload: text }),
  toggleTodo: (id) => ({ type: 'TOGGLE_TODO', payload: id }),
  deleteTodo: (id) => ({ type: 'DELETE_TODO', payload: id })
}

// reducer.js
const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.payload, completed: false }]
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
      )
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.payload)
    default:
      return state
  }
}

// TodoContext.js
const TodoContext = React.createContext()

export function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, [])
  
  return (
    <TodoContext.Provider value={{ todos, dispatch }}>
      {children}
    </TodoContext.Provider>
  )
}

export function useTodos() {
  const context = useContext(TodoContext)
  if (!context) {
    throw new Error('useTodos must be used within TodoProvider')
  }
  return context
}

// TodoList.js
function TodoList() {
  const { todos, dispatch } = useTodos()
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span
            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
            onClick={() => dispatch(todoActions.toggleTodo(todo.id))}
          >
            {todo.text}
          </span>
          <button onClick={() => dispatch(todoActions.deleteTodo(todo.id))}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  )
}

四、性能优化 Hooks

1. useCallback - 缓存回调函数

javascript
// ========== 问题场景 ==========
function Parent() {
  const [count, setCount] = useState(0)
  
  // ✗ 每次渲染都创建新函数
  const handleClick = () => {
    console.log('clicked')
  }
  
  return (
    <>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </>
  )
}

function Child({ onClick }) {
  // 即使 props 没变,也会重新渲染(因为 onClick 引用变了)
  console.log('Child rendered')
  return <button onClick={onClick}>Click me</button>
}


// ========== 解决方案 ==========
function Parent() {
  const [count, setCount] = useState(0)
  
  // ✓ 只有依赖变化时才创建新函数
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, []) // 空依赖,函数引用永远不变
  
  return (
    <>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </>
  )
}

// 配合 React.memo 使用
const Child = React.memo(function Child({ onClick }) {
  console.log('Child rendered')
  return <button onClick={onClick}>Click me</button>
})


// ========== 带依赖的 useCallback ==========
function SearchBox({ onSearch }) {
  const [query, setQuery] = useState('')
  
  // ✓ 依赖 query,query 变化时更新回调
  const handleSearch = useCallback(() => {
    onSearch(query)
  }, [query, onSearch])
  
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button onClick={handleSearch}>Search</button>
    </div>
  )
}


// ========== 常见误区 ==========

// 误区 1: 滥用 useCallback
function Component() {
  // ✗ 没必要:函数没有传递给子组件
  const handleClick = useCallback(() => {
    console.log('click')
  }, [])
  
  // ✓ 普通函数即可
  const handleClick = () => {
    console.log('click')
  }
}

// 原则:只有传递给子组件或作为依赖时才用 useCallback


// 误区 2: 忘记添加依赖
function Component({ userId }) {
  // ✗ userId 变化但回调不更新
  const fetchUser = useCallback(() => {
    api.getUser(userId)
  }, []) // 缺少 userId
  
  // ✓ 正确
  const fetchUser = useCallback(() => {
    api.getUser(userId)
  }, [userId])
}

2. useMemo - 缓存计算结果

javascript
// ========== 基础用法 ==========
function ExpensiveComponent({ items, filter }) {
  // ✗ 每次渲染都重新计算
  const filteredItems = items.filter(item => item.includes(filter))
  
  // ✓ 只有依赖变化时才重新计算
  const filteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter))
  }, [items, filter])
  
  return <List items={filteredItems} />
}


// ========== 实际应用场景 ==========

// 场景 1: 复杂计算
function DataTable({ data, sortBy }) {
  const sortedData = useMemo(() => {
    console.log('排序计算...')
    return [...data].sort((a, b) => {
      if (sortBy === 'name') return a.name.localeCompare(b.name)
      if (sortBy === 'age') return a.age - b.age
      return 0
    })
  }, [data, sortBy])
  
  return <Table data={sortedData} />
}


// 场景 2: 创建对象引用
function UserProfile({ user }) {
  // ✗ 每次渲染创建新对象,导致子组件无效重渲染
  const style = { color: user.active ? 'green' : 'red' }
  
  // ✓ 缓存对象引用
  const style = useMemo(() => ({
    color: user.active ? 'green' : 'red'
  }), [user.active])
  
  return <div style={style}>{user.name}</div>
}


// 场景 3: 避免重复 API 调用
function SearchResults({ query }) {
  const fetchResults = useMemo(() => {
    return debounce(async (q) => {
      const results = await api.search(q)
      setResults(results)
    }, 300)
  }, []) // 只创建一次 debounced 函数
  
  useEffect(() => {
    fetchResults(query)
  }, [query, fetchResults])
}


// ========== 性能对比 ==========
function Benchmark() {
  const [count, setCount] = useState(0)
  const items = Array.from({ length: 10000 }, (_, i) => i)
  
  // 不使用 useMemo
  const start1 = performance.now()
  const result1 = items.filter(i => i % 2 === 0)
  const time1 = performance.now() - start1
  
  // 使用 useMemo
  const start2 = performance.now()
  const result2 = useMemo(() => {
    return items.filter(i => i % 2 === 0)
  }, [items])
  const time2 = performance.now() - start2
  
  return (
    <div>
      <p>Without useMemo: {time1.toFixed(2)}ms</p>
      <p>With useMemo: {time2.toFixed(2)}ms (后续渲染接近 0ms)</p>
      <button onClick={() => setCount(count + 1)}>Re-render</button>
    </div>
  )
}

3. useRef - 持久化引用

javascript
// ========== 基础用法 ==========
function TextInput() {
  const inputRef = useRef(null)
  
  useEffect(() => {
    inputRef.current.focus()
  }, [])
  
  return <input ref={inputRef} />
}


// ========== 保存可变值(不触发重渲染)==========
function Timer() {
  const [count, setCount] = useState(0)
  const intervalRef = useRef(null)
  
  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1)
    }, 1000)
    
    return () => clearInterval(intervalRef.current)
  }, [])
  
  const stopTimer = () => {
    clearInterval(intervalRef.current)
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={stopTimer}>Stop</button>
    </div>
  )
}


// ========== 记录前一个值 ==========
function usePrevious(value) {
  const ref = useRef()
  
  useEffect(() => {
    ref.current = value
  }, [value])
  
  return ref.current
}

function Counter() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)
  
  return (
    <div>
      <p>Now: {count}, before: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}


// ========== 避免不必要的 Effect 执行 ==========
function SearchInput({ onSearch }) {
  const [query, setQuery] = useState('')
  const isFirstRender = useRef(true)
  
  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false
      return
    }
    
    // 跳过首次渲染
    onSearch(query)
  }, [query, onSearch])
  
  return <input value={query} onChange={e => setQuery(e.target.value)} />
}


// ========== useRef vs useState ==========
function Comparison() {
  // useState: 更新触发重渲染
  const [countState, setCountState] = useState(0)
  
  // useRef: 更新不触发重渲染
  const countRef = useRef(0)
  
  const incrementState = () => setCountState(countState + 1)
  const incrementRef = () => {
    countRef.current += 1
    console.log(countRef.current) // 立即看到新值
  }
  
  return (
    <div>
      <p>State: {countState}(更新会重渲染)</p>
      <p>Ref: {countRef.current}(更新不会重渲染,显示的是旧值)</p>
      <button onClick={incrementState}>State++</button>
      <button onClick={incrementRef}>Ref++</button>
    </div>
  )
}

五、自定义 Hooks

1. 自定义 Hooks 规范

javascript
// ========== 命名规范 ==========
// ✓ 必须以 use 开头
function useUserData() { /* ... */ }
function useFetch() { /* ... */ }

// ✗ 错误命名
function getUserData() { /* ... */ }
function fetchData() { /* ... */ }


// ========== 基本结构 ==========
function useCustomHook(initialValue) {
  // 1. 声明状态
  const [state, setState] = useState(initialValue)
  
  // 2. 副作用
  useEffect(() => {
    // 副作用逻辑
  }, [])
  
  // 3. 缓存回调
  const callback = useCallback(() => {
    // 回调逻辑
  }, [])
  
  // 4. 返回值
  return {
    state,
    setState,
    callback
  }
}


// ========== 组合使用其他 Hooks ==========
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth)
  
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])
  
  return width
}

function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title
  }, [title])
}

function MyComponent() {
  const width = useWindowWidth()
  useDocumentTitle(`Width: ${width}`)
  
  return <div>Window width: {width}</div>
}

2. 实用自定义 Hooks

javascript
// ========== useLocalStorage ==========
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(error)
      return initialValue
    }
  })
  
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.error(error)
    }
  }
  
  return [storedValue, setValue]
}

// 使用
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current theme: {theme}
    </button>
  )
}


// ========== useDebounce ==========
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value)
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)
    
    return () => {
      clearTimeout(timer)
    }
  }, [value, delay])
  
  return debouncedValue
}

// 使用
function SearchBox() {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, 300)
  
  useEffect(() => {
    if (debouncedQuery) {
      fetchResults(debouncedQuery)
    }
  }, [debouncedQuery])
  
  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
    />
  )
}


// ========== useAsync ==========
function useAsync(asyncFunction, immediate = true) {
  const [status, setStatus] = useState('idle')
  const [value, setValue] = useState(null)
  const [error, setError] = useState(null)
  
  const execute = useCallback(() => {
    setStatus('pending')
    setValue(null)
    setError(null)
    
    return asyncFunction()
      .then(response => {
        setValue(response)
        setStatus('success')
      })
      .catch(error => {
        setError(error)
        setStatus('error')
      })
  }, [asyncFunction])
  
  useEffect(() => {
    if (immediate) {
      execute()
    }
  }, [execute, immediate])
  
  return { execute, status, value, error }
}

// 使用
function UserProfile({ userId }) {
  const { status, value: user, error, execute } = useAsync(
    () => fetchUser(userId),
    false
  )
  
  useEffect(() => {
    execute()
  }, [userId, execute])
  
  if (status === 'pending') return <Spinner />
  if (status === 'error') return <Error message={error.message} />
  if (status === 'success') return <div>{user.name}</div>
  
  return null
}


// ========== useEventListener ==========
function useEventListener(eventName, handler, element = window) {
  const savedHandler = useRef()
  
  useEffect(() => {
    savedHandler.current = handler
  }, [handler])
  
  useEffect(() => {
    const isSupported = element && element.addEventListener
    if (!isSupported) return
    
    const eventListener = (event) => savedHandler.current(event)
    element.addEventListener(eventName, eventListener)
    
    return () => {
      element.removeEventListener(eventName, eventListener)
    }
  }, [eventName, element])
}

// 使用
function KeyTracker() {
  const [keys, setKeys] = useState([])
  
  useEventListener('keydown', (event) => {
    setKeys(prev => [...prev, event.key])
  })
  
  return <div>Pressed keys: {keys.join(', ')}</div>
}


// ========== useIntersectionObserver ==========
function useIntersectionObserver(options = {}) {
  const [isIntersecting, setIsIntersecting] = useState(false)
  const targetRef = useRef(null)
  
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting)
    }, options)
    
    if (targetRef.current) {
      observer.observe(targetRef.current)
    }
    
    return () => {
      if (targetRef.current) {
        observer.unobserve(targetRef.current)
      }
    }
  }, [options])
  
  return [targetRef, isIntersecting]
}

// 使用
function LazyImage({ src, alt }) {
  const [ref, isVisible] = useIntersectionObserver({ threshold: 0.1 })
  
  return (
    <div ref={ref}>
      {isVisible ? (
        <img src={src} alt={alt} />
      ) : (
        <div className="placeholder">Loading...</div>
      )}
    </div>
  )
}

六、Hooks 规则与原理

1. Hooks 两条铁律

javascript
// ========== 规则 1: 只能在顶层调用 ==========

// ✗ 错误:在条件语句中调用
function Component({ condition }) {
  if (condition) {
    const [state, setState] = useState(0) // ❌
  }
  
  const [count, setCount] = useState(0) // ✅
}

// ✗ 错误:在循环中调用
function Component({ items }) {
  items.forEach(item => {
    const [state, setState] = useState(0) // ❌
  })
}

// ✗ 错误:在嵌套函数中调用
function Component() {
  function handleClick() {
    const [state, setState] = useState(0) // ❌
  }
}

// ✓ 正确:始终在顶层调用
function Component({ condition, items }) {
  const [state1, setState1] = useState(0) // ✅
  const [state2, setState2] = useState(0) // ✅
  
  if (condition) {
    // 可以使用 state
    console.log(state1)
  }
}


// ========== 规则 2: 只能在 React 函数中调用 ==========

// ✗ 错误:在普通 JavaScript 函数中调用
function regularFunction() {
  const [state, setState] = useState(0) // ❌
}

// ✓ 正确:在 React 函数组件中调用
function MyComponent() {
  const [state, setState] = useState(0) // ✅
}

// ✓ 正确:在自定义 Hooks 中调用
function useCustomHook() {
  const [state, setState] = useState(0) // ✅
}


// ========== ESLint 插件 ==========
// 安装 eslint-plugin-react-hooks
npm install eslint-plugin-react-hooks --save-dev

// .eslintrc.json
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

2. Hooks 实现原理

javascript
// ========== 简化版 Hooks 实现 ==========

let hooks = []
let currentHookIndex = 0

function useState(initialValue) {
  const hookIndex = currentHookIndex
  
  // 首次渲染:初始化
  if (hooks[hookIndex] === undefined) {
    hooks[hookIndex] = {
      state: initialValue,
      queue: []
    }
  }
  
  const hook = hooks[hookIndex]
  
  // setState 函数
  const setState = (newValue) => {
    const value = typeof newValue === 'function' 
      ? newValue(hook.state) 
      : newValue
    
    hook.state = value
    // 触发重新渲染
    render()
  }
  
  currentHookIndex++
  return [hook.state, setState]
}

function useEffect(callback, deps) {
  const hookIndex = currentHookIndex
  
  if (hooks[hookIndex] === undefined) {
    hooks[hookIndex] = { deps: undefined, cleanup: undefined }
  }
  
  const hook = hooks[hookIndex]
  
  // 检查依赖是否变化
  const hasChanged = !deps || !hook.deps || 
    deps.some((dep, i) => dep !== hook.deps[i])
  
  if (hasChanged) {
    // 执行清理
    if (hook.cleanup) {
      hook.cleanup()
    }
    
    // 执行 effect
    hook.cleanup = callback()
    hook.deps = deps
  }
  
  currentHookIndex++
}

function render(Component) {
  // 重置索引
  currentHookIndex = 0
  
  // 渲染组件
  const element = Component()
  
  // 返回 JSX
  return element
}

// 使用示例
function Counter() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    document.title = `Count: ${count}`
  }, [count])
  
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

// 首次渲染
render(Counter)
// hooks = [{ state: 0, queue: [] }, { deps: [0], cleanup: undefined }]

// 点击按钮
setCount(1)
// 重新渲染
render(Counter)
// hooks = [{ state: 1, queue: [] }, { deps: [1], cleanup: undefined }]

3. 为什么 Hooks 不能有条件调用?

┌──────────────────────────────────────────────────────────┐
│           Hooks 为什么必须在顶层调用                      │
└──────────────────────────────────────────────────────────┘

正确调用顺序:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一次渲染:
function Component() {
  useState(0)      // Hook 1 → hooks[0]
  useEffect(fn)    // Hook 2 → hooks[1]
  useState('')     // Hook 3 → hooks[2]
}

第二次渲染:
function Component() {
  useState(0)      // 读取 hooks[0] ✓
  useEffect(fn)    // 读取 hooks[1] ✓
  useState('')     // 读取 hooks[2] ✓
}

所有 Hook 都能正确对应 ✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

错误调用顺序:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一次渲染 (condition = true):
function Component() {
  if (true) {
    useState(0)    // Hook 1 → hooks[0]
  }
  useState('')     // Hook 2 → hooks[1]
}

第二次渲染 (condition = false):
function Component() {
  if (false) {
    // useState(0) 被跳过
  }
  useState('')     // 读取 hooks[0] ❌
                   // 期望是字符串,实际是数字!
}

Hook 对应关系错乱 → Bug! ✗
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

解决方案:
✓ 始终在顶层调用
✓ 使用 ESLint 插件检测
✓ 条件逻辑放在 Hook 内部

七、常见面试题

题目 1:useState 和 useReducer 的区别?

javascript
// 标准回答要点:

/*
相同点:
- 都用于管理组件状态
- 更新都会触发重渲染
- 都支持函数式更新

区别:

1. 复杂度
   - useState: 适合简单状态(布尔值、数字、字符串)
   - useReducer: 适合复杂状态(对象、数组、多个子值)

2. 更新逻辑
   - useState: 直接设置新值
   - useReducer: 通过 dispatch action,由 reducer 纯函数计算新状态

3. 可预测性
   - useState: 更新分散在各处
   - useReducer: 所有更新逻辑集中在 reducer,易于调试和测试

4. 性能
   - useState: 每次更新都创建新的 setter
   - useReducer: dispatch 引用稳定,可作为 useCallback 依赖

选择建议:
- 状态独立且简单 → useState
- 状态之间有逻辑关系 → useReducer
- 下一个状态依赖前一个状态 → useReducer
- 需要集中管理更新逻辑 → useReducer
*/


// 示例对比

// useState 适合简单场景
function Toggle() {
  const [on, setOn] = useState(false)
  return <button onClick={() => setOn(!on)}>{on ? 'ON' : 'OFF'}</button>
}

// useReducer 适合复杂场景
function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 })
  
  // 所有更新逻辑集中在 reducer
  // ADD_ITEM, REMOVE_ITEM, UPDATE_QUANTITY 等
}

题目 2:useEffect 和 useLayoutEffect 的区别?

javascript
// 标准回答要点:

/*
执行时机:
- useEffect: 浏览器绘制后异步执行(不阻塞渲染)
- useLayoutEffect: DOM 更新后、浏览器绘制前同步执行(阻塞渲染)

使用场景:
- useEffect: 大多数副作用(数据获取、订阅、日志)
- useLayoutEffect: 需要同步测量 DOM 或同步更新 DOM

性能影响:
- useEffect: 不会阻塞页面渲染,性能更好
- useLayoutEffect: 会阻塞渲染,可能导致卡顿

选择建议:
- 默认使用 useEffect
- 只有在看到闪烁问题时才改用 useLayoutEffect
*/


// 示例:测量 DOM

// useEffect - 可能闪烁
function MeasureWithEffect() {
  const [height, setHeight] = useState(0)
  const ref = useRef(null)
  
  useEffect(() => {
    // 浏览器已经绘制,用户看到初始高度 0
    // 然后更新为实际高度,产生闪烁
    setHeight(ref.current.offsetHeight)
  }, [])
  
  return <div ref={ref}>Content</div>
}

// useLayoutEffect - 无闪烁
function MeasureWithLayoutEffect() {
  const [height, setHeight] = useState(0)
  const ref = useRef(null)
  
  useLayoutEffect(() => {
    // 浏览器绘制前就更新了,用户看不到中间状态
    setHeight(ref.current.offsetHeight)
  }, [])
  
  return <div ref={ref}>Content</div>
}

题目 3:如何避免 useEffect 无限循环?

javascript
// 标准回答要点:

/*
常见原因:

1. 依赖项在 effect 中被更新
   - 解决:移除不必要的依赖或使用函数式更新

2. 对象/数组/函数作为依赖,每次都是新引用
   - 解决:使用 useMemo/useCallback 稳定引用

3. 在 effect 中调用 setState 触发重新渲染
   - 解决:添加正确的依赖数组或条件判断

预防措施:
- 启用 eslint-plugin-react-hooks
- 仔细审查依赖数组
- 使用 useRef 保存不需要触发更新的值
- 考虑将逻辑提取到自定义 Hooks
*/


// 示例:修复无限循环

// ✗ 无限循环
function BadExample() {
  const [data, setData] = useState(null)
  
  useEffect(() => {
    fetchData().then(setData)
  }, [data]) // data 变化 → effect 执行 → setData → data 变化 → ...
}

// ✓ 修复方案 1:移除依赖
function GoodExample1() {
  const [data, setData] = useState(null)
  
  useEffect(() => {
    fetchData().then(setData)
  }, []) // 仅挂载时执行
}

// ✓ 修复方案 2:使用标志位
function GoodExample2() {
  const [data, setData] = useState(null)
  const fetched = useRef(false)
  
  useEffect(() => {
    if (!fetched.current) {
      fetched.current = true
      fetchData().then(setData)
    }
  }, [data])
}

题目 4:自定义 Hooks 的优势是什么?

javascript
// 标准回答要点:

/*
优势:

1. 逻辑复用
   - 取代 HOC 和 Render Props
   - 避免组件树嵌套过深(Wrapper Hell)

2. 关注点分离
   - 相关逻辑聚合在一起
   - 不像生命周期那样分散

3. 易于测试
   - 纯函数,不依赖组件实例
   - 可以单独测试 Hook 逻辑

4. 社区生态
   - 大量现成的自定义 Hooks
   - react-use、ahooks 等库

5. 类型安全
   - TypeScript 支持良好
   - 完整的类型推断

最佳实践:
- 以 use 开头命名
- 可以组合其他 Hooks
- 返回必要的状态和方法
- 提供清晰的类型定义
*/

八、面试标准回答

React Hooks 是 React 16.8 引入的新特性,让函数组件能够使用状态和其他 React 特性,无需编写 class 组件。

核心优势包括:

  1. 逻辑复用更简单:通过自定义 Hooks 替代 HOC 和 Render Props,避免组件嵌套地狱
  2. 代码组织更清晰:相关逻辑聚合在一起,而不是分散在生命周期方法中
  3. 学习曲线更低:无需理解 class、this 绑定等复杂概念
  4. 更小的打包体积:函数组件比 class 组件更容易压缩和优化

常用 Hooks 分类

  • 状态管理:useState、useReducer
  • 副作用处理:useEffect、useLayoutEffect
  • 性能优化:useCallback、useMemo、useRef
  • 上下文访问:useContext
  • 其他:useImperativeHandle、useDebugValue

使用规则有两条铁律:

  1. 只能在顶层调用,不能在条件、循环或嵌套函数中调用
  2. 只能在 React 函数组件或自定义 Hooks 中调用

性能优化方面,我会:

  • 使用 useCallback 缓存传递给子组件的回调函数
  • 使用 useMemo 缓存昂贵的计算结果
  • 使用 React.memo 配合稳定的 props 引用
  • 避免在 render 中创建新的对象/数组/函数

自定义 Hooks 是逻辑复用的最佳实践,我常用它来封装数据获取、表单处理、本地存储等功能,让组件更专注于 UI 渲染。

常见问题包括闭包陷阱、依赖数组遗漏、无限循环等,通过 ESLint 插件和仔细的代码审查可以避免这些问题。


九、记忆口诀

Hooks 歌诀:

Hooks 让函数变强大,
状态副作用都不怕。
两条规则要牢记,
顶层调用别落下!

useState 管状态,
useEffect 处理副作用。
useCallback 缓存函数,
useMemo 缓存计算结果好!

useRef 存引用,
useContext 跨组件通。
useReducer 复杂态,
自定义 Hooks 逻辑重用!

性能优化三板斧:
memo useCallback useMemo。
依赖数组仔细看,
无限循环要预防!

Hooks 虽好别滥用,
简单场景 useState 够。
复杂逻辑 useReducer,
组合使用最优秀!

十、推荐资源


十一、总结一句话

  • Hooks 核心: 函数组件 + 状态能力 = Class 组件的现代替代 🎯
  • 性能优化: useCallback + useMemo + memo = 避免不必要渲染
  • 逻辑复用: 自定义 Hooks + 组合 = 告别 HOC 嵌套地狱
最近更新